Der Microservices & Services Track auf der BASTA! 2017
„Microservices“ ist ein großes Wort, auch wenn es so klein klingt. Im Wesentlichen geht es um Dienste, und zwar um kleine Dienste. Wie klein die sein sollen, ist eine Frage, die oft und gern gestellt wird, und zu der es keine einfache, kurze und pauschale Antwort gibt. Wie so oft muss zunächst ein Verständnis her zum Pattern, bevor Fragen im Detail beantwortet werden können. Und bei Patterns ist es zum Verständnis immer wichtig, den Zweck der Sache zu beleuchten. Also sollten wir fragen: „Warum machen wir das?“, und nicht: „Wie geht das?“
Die Idee der Microservices stammt aus der Erkenntnis, dass Softwareentwicklung und deren Planung oft etwas realitätsfern sind. Ähnlich den agilen Ideen setzt man sich hier zum Ziel, eine große Aufgabe in kleine Teile zu unterteilen. Bei agiler Planung geht es um Abläufe und bei Microservices um Strukturen. Wo bisher Softwareplanung oft monolithisch betrachtet wurde, soll nun alles logisch in kleine Elemente zerlegt werden. Früher gab es Pläne, im Laufe der nächsten drei Jahre ein gewaltiges Stück Software zu bauen, das dann für die nächsten fünfzehn Jahre erfolgreich verkauft werden sollte. Irgendwann funktionierte das vielleicht auch mal, und als Unternehmensplan ist die Vorlage „fünfzehn Jahre gute Software verkaufen“ auch sicher sinnvoll. Aber mit der Realität von Kunden wie auch Entwicklern lässt sich dieser Ansatz auf technischer Ebene heute nicht mehr gut vereinbaren.
Klein machen
Die Probleme kennen Entwickler wie auch Softwaredesigner gut. Um ein Stück Software zu erzeugen, brauchen wir eine Plattform, eine Programmiersprache, verschiedene hilfreiche Libraries mit Zusatzfunktionalität und natürlich Entwickler, die sich ausführlich in diese Bauteile einarbeiten, zu Experten heranwachsen und langfristig für die Firma am Projekt arbeiten. In der wirklichen Welt ändern sich die Anforderungen allerdings leider ständig: Die Plattform der Wahl ist in ein paar Jahren vielleicht nicht mehr diejenige, mit der die meisten Endanwender arbeiten. Die Programmiersprache stellt sich längerfristig eventuell als falsche Wahl heraus, zumindest, wenn man gleichzeitig von neuen Entwicklungen profitieren möchte, die andere Entwickler verfügbar machen. Die Libraries sind meist an Plattform und Sprache gebunden, und die meisten Entwickler haben zwar gewissen Respekt vor dem Neuen. Sie wollen sich aber persönlich einen Platz in ihrem Job sichern, indem sie technisch am Ball bleiben und sind daher nicht endlos bereit, einem Weg zu folgen, der aus ihrer Sicht nicht mehr dem Stand der Technik entspricht. So wird die Pflege einer alten Software im Laufe der Zeit immer teurer, die Kunden werden immer unzufriedener, während die langfristige Planung und die monolithische Architektur intern weitreichende Überarbeitungen oder gar eine Neuentwicklung zum Tabuthema werden lassen.
Grundsätzlich kann ein Dienst auf jeder Plattform, in jeder Programmiersprache implementiert werden.
Microservices setzen auf Unabhängigkeit zwischen den Modulen eines Softwaresystems. Es sollen Dienste gebaut werden, die einzelne Aufgaben übernehmen und die voneinander entkoppelt sind. Die Aufgaben eines Dienstes sollen kurz und bündig definierbar sein; meistens denken wir dabei an eine einzelne Funktion mit einer Aufrufschnittstelle. Dadurch beantwortet sich die Frage nach der Größe eines kleinen Dienstes. Da jeder Dienst von allen anderen möglichst unabhängig ist, ergeben sich weitreichende Freiheiten: Plattform, Programmiersprache und Hilfs-Libraries können womöglich pro Dienst neu definiert werden. In einem neuen Konzept werden Sie vermutlich nicht so weit gehen wollen, denn immerhin gilt es, den Kenntnisstand von Mitarbeitern zu berücksichtigen, eventuell vorhandenen Code wiederzuverwenden und dergleichen mehr. Aber Sie gewinnen die Freiheit, später andere Entscheidungen treffen zu können – oder in gewissen Einzelfällen auch sofort. .NET Version X ist gerade erschienen und kann irgendwas Neues, von dem Sie technisch profitieren könnten? Dann kann ein Dienst oder eine Gruppe von Diensten auf .NET Version X umgestellt werden und mit der neuen Technik arbeiten, ohne dass andere Teile Ihres Systems geändert werden müssen. Vielleicht ist der Arbeitsmarkt plötzlich überschwemmt von Programmierern, die gut Python können – davon können Sie als Unternehmen profitieren, denn Teile eines Microservices-Systems können natürlich in Python programmiert werden.
Wenn nötig, einfach neu
Mit Microservices wird Wegwerfen zur Kultur. Nicht unbedingt zur Tugend, denn Neumachen kostet immer Geld, aber eben nicht mehr zur Unmöglichkeit. Wie Firmen in anderen Geschäftsfeldern etwa ein Auto bis zur Unkenntlichkeit umbauen, durch den Einbau von Spezialteilen für die Rallyeteilnahme, oder so wie eine PCI-Karte viele Jahre nach Erfindung des PCI-Busses mit einer Hardware kommunizieren kann, die es damals noch gar nicht gab, so lässt sich eine Software, die auf Microservices basiert, flexibel und modular über Jahre hinweg pflegen.
Diese Überlegungen bringen mich nun zurück zur eingangs angesprochenen Frage: „Wie sehen diese vielen Dienste denn aus?“ Die Antwort dazu ist, natürlich, ebenfalls vielschichtig. Grundsätzlich kann ein Dienst auf jeder Plattform, in jeder Programmiersprache implementiert werden. Technisch handelt es sich dabei ja „nur“ um eine Funktion (oder womöglich um ein „Netz“ von Funktionen), die einige Parameter übergeben bekommt und ein Resultat liefert. Das ist ein funktionaler Ansatz im Sinne der funktionalen Programmierung, wo Interaktion zwischen Funktionen außerhalb des simplen Schemas von Übergabeparametern und Rückgabewerten verpönt ist.
Es sollen Dienste gebaut werden, die einzelne Aufgaben übernehmen und die voneinander entkoppelt sind.
;
Interaktion muss her
Von größerem technischen Interesse als die Implementierung einzelner Dienste ist deshalb die Frage, wie diese Dienste miteinander interagieren. Um die Flexibilität zu erreichen, die das Pattern verspricht, muss dazu eine Aufrufschnittstelle her, die sich „von außen“, also von anderen Diensten, ansprechen lässt. Diese Schnittstelle muss plattformunabhängig sein, auch was den Austausch von Daten bei Über- und Rückgabe angeht. Verbreitet wird zu diesem Zweck heute die Kombination von REST als URL-basiertem Aufrufschema und JSON als effizientem Datenübertragungsformat eingesetzt. Diese Kombination ist technisch simpel, und es gibt Libraries auf sehr vielen Plattformen, die den Umgang mit REST und JSON einfach machen. Selbst wenn es solche Libraries nicht geben sollte, wären diese Mechanismen, die auf HTTP bzw. einem simplen Textformat basieren, noch immer mit wenig Aufwand nutzbar. Auf komplexe Protokolle aus der SOA-Welt wird meist bewusst verzichtet, um die Einstiegshürde so niedrig wie möglich zu halten.
In .NET können Sie einen Dienst, der mit REST und JSON arbeiten soll, einfach mithilfe von Microsofts ASP.NET-Web-API verfügbar machen. Das funktioniert auch mit .NET Core, sodass Sie plattformunabhängig sein und auch Dienste mit Docker paketieren und in beliebigen Cloud-Umgebungen laufen lassen können. Alternativ lassen sich dieselben Ergebnisse mit Nancy oder anderen Libraries auf der .NET-Plattform erzielen. Ähnliche Libraries gibt es auch für andere Plattformen: Express für JavaScript und Node.js, Django für Python, Sinatra für Ruby und viele andere mehr. Wie auch Microsoft ASP.NET MVC haben die genannten Libraries Funktionalität weit jenseits der einfachen Erstellung von REST/JSON-Diensten, sodass sie sich vielfältig einsetzen lassen.
Nachdem Dienste nun „adressierbar“ sind, besteht der nächste Schritt darin, über die Struktur des gesamten Anwendungssystems nachzudenken. Um einen Dienst direkt mit einem anderen kommunizieren zu lassen, muss zumindest einer der Dienste wissen, wo der andere zu finden ist. In einem komplexen Netz von Diensten ist diese Anforderung schwierig umzusetzen, und die Situation wird wesentlich komplizierter, wenn etwa Mechanismen zur Lastverteilung oder zur Skalierung von Diensten oder Dienstgruppen zum Einsatz kommen. Daher nutzen viele Microservices-Konzepte einen Vermittler, englisch Broker, der Nachrichten entgegennehmen und an Dienste weiterleiten kann. Der Broker ist natürlich selbst ein Dienst und passt somit gut ins Konzept.
Broker vermitteln zwischen Diensten
Broker schreiben die meisten Programmierer nicht selbst, da es bereits viele vorhandene Lösungen gibt. Zum Beispiel sind das Message-Queue-Systeme wie RabbitMQ, Redis oder Microsoft MSMQ, oder auch Dienste, die von einer Cloud verfügbar gemacht werden, wie Amazon SQS oder Azure Queue Storage bzw. Service Bus. Viele Konzepte setzen direkt auf eine dieser Technologien, andere verwenden eine Abstraktionsschicht wie NServiceBus für .NET, um sich nicht von einer bestimmten Broker-Implementation abhängig zu machen. Viele Lösungen unterstützen auch das standardisierte Protokoll AMQP, mit dem ein Client einen Broker ansprechen kann, ohne dessen Implementatierung zu kennen. Letztlich ist es auch möglich, einen Broker wiederum per REST anzusprechen, um das Gesamtsystem so offen wie möglich zu halten.
Von größerem technischen Interesse als die Implementierung einzelner Dienste ist deshalb die Frage, wie diese Dienste miteinander interagieren.
Wenn Sie meine Kolumne regelmäßig verfolgen, erinnern Sie sich bestimmt an meinen Artikel zum Thema Akka.NET [1], der Implementierung eines Aktorenframeworks für .NET. Ein System, das auf Microservices basiert und einen Broker zum Austausch von Informationen zwischen Diensten verwendet, zeigt bemerkenswerte Parallelen zu den Ideen der Aktoren. Letztere verarbeiten ebenfalls Nachrichten und sind voneinander vollständig entkoppelt, während die Infrastruktur für den Austausch von Informationen durch asynchrone Mechanismen sorgt – und Akka.NET bietet eigene Möglichkeiten zur verteilten Ausführung von Aktoren.
Aktoren sind auch Dienste, aber anders
Es gibt zwei wichtige Unterschiede zwischen typischen Aktorensystemen und Microservices. Erstens bietet ein Aktorensystem viel organisatorische Funktionalität, die den Betrieb des Gesamtsystems stabiler macht. Es werden Hierarchien von Aktoren aufgebaut, in denen ein „Parent“ jeweils für den Betrieb seiner „Kinder“ verantwortlich ist, und es gibt Überwachungs- und Benachrichtigungsfunktionen, mit deren Hilfe der Lebenszyklus einzelner Aktoren kontrolliert werden kann. Damit sind Aktoren, wenn auch funktional eigenständig, wesentlich stärker in das Gesamtsystem eingebunden, als das für Microservices wünschenswert ist. Letztlich sind diese Grenzen natürlich fließend und manche Microservices-Systeme bauen ebenfalls Gruppen von Diensten auf, die stark voneinander abhängig sind.
Der zweite wichtige Unterschied ergibt sich aus dem ersten: Die Protokolle, die ein Aktorensystem zum Datenaustausch zwischen Aktoren verwendet, sind oft komplexer und darauf ausgerichtet, relevante Verwaltungsinformationen zusammen mit den Nutzdaten zu übertragen. Technisch ist es durchaus möglich, beliebige Kommunikationsprotokolle in einem Aktorensystem zu verwenden und manche Entwickler benutzen Aktorensysteme gemeinsam mit Message-Queue- oder Service-Bus-Varianten. Allerdings darf nicht übersehen werden, dass die Versprechungen von Aktorensystemen in Hinsicht auf die langfristige Laufstabilität letztlich auf den implementierten Konzepten zur Dienstüberwachung und Lebenszyklusverwaltung basieren. Während also ein externer Dienst sich technisch durchaus in ein solches System „einklinken“ kann, muss er sich dann entsprechend den Erwartungen dieses Systems verhalten und er verliert dabei einen Großteil der Unabhängigkeit, die ein eigenständiger Dienst haben sollte.
Daher nutzen viele Microservices-Konzepte einen Vermittler, englisch Broker, der Nachrichten entgegennehmen und an Dienste weiterleiten kann. Der Broker ist natürlich selbst ein Dienst und passt somit gut ins Konzept.
Trotz dieser Betrachtungen lassen sich Aktoren natürlich innerhalb eines Microservices-Systems einsetzen. Immerhin geht es dort besonders um den Einsatz geeigneter Technologien für jedes einzelne Teilsystem, und mithilfe von Aktoren lassen sich etwa Datenverarbeitungsalgorithmen stabil und modular umsetzen, deren externe Schnittstellen dann in Form eines oder mehrerer Dienste angeboten werden können. Mit Akka.Cluster gibt es in diesem Bereich sogar ein Modul, das auf Basis von Aktorensystemen automatisch Gruppen von Aktorensystemen verteilen kann, basierend allerdings noch immer auf den komplexen, wenn auch optimierten, internen Protokollen. Die Firma Lightbend, involviert in die Entwicklung von Akka für Java, bietet mit Lagom ein neues Framework an, das auf Basis von Akka Microservices implementiert. Diese Lösung ist derzeit für .NET meines Wissens noch nicht verfügbar.
Zum Abschluss möchte ich ein Framework ansprechen, das ich für eine sehr leistungsfähige Basis für Microservices halte. Dabei handelt es sich um das in JavaScript programmierte Seneca, das eine besonders interessante Kombination von Features bietet. Sie konfigurieren beim Einsatz von Seneca Dienste, die mithilfe von Pattern-Matching auf Nachrichten reagieren können und bei deren Empfang ihre Logik ausführen und Resultate liefern. Der Umgang mit dem API von Seneca ist sehr einfach und so gelingt es schnell, ein Netzwerk von Diensten aufzubauen. Nun hat Seneca weiterhin die Fähigkeit, auf Basis der Patterns, auf die die Dienste antworten, auch Routen zu definieren, die automatisch bestimmte Nachrichten über externe Wege weiterleiten, die auf Basis zahlreicher Plug-ins flexibel konfigurierbar sind. Natürlich können Sie Dienste über REST-URLs zugreifbar machen, aber auch die Verwendung einer Message Queue ist mit zwei Zeilen Code eingerichtet. Jedes Dienstprogramm, groß oder klein, kann so auf einfache Weise manche Dienste direkt lokal ansprechen und andere „remote“ über unterschiedliche Protokolle, ohne dass ein Dienst selbst etwas davon wissen muss, wie das Gesamtsystem aufgebaut ist.
Fazit
Microservices stellen ein mächtiges und vielschichtiges Pattern dar, von dem beinahe jedes Anwendungssystem profitieren kann. Die Frage nach der Kommunikation in einem solchen System ist eine der wichtigsten und die Antworten sind vielfältig, weil sie unterschiedliche Anwendungsfälle abdecken. Es empfiehlt sich in jedem Fall, nach Hilfsmitteln auf den Plattformen der Wahl Ausschau zu halten, da die Verwendung von Standardsystemen wie RabbitMQ, Cloud-Infrastruktur wie Amazon SQS oder Azure Service Bus und Libraries wie Seneca Ihnen viel Arbeit ersparen kann.
[1] „Olis bunte Welt der IT“, Windows Developer 11.2016, S. 14
;
Architecture Day auf der BASTA!
● Domain-driven Design – Basis für Microservices und Anwenderglück
;